#!/bin/bash

# Copyright (c) 2023 Douglas Gilbert.
# SPDX-License-Identifier: BSD-2-Clause

# This script is designed to print out pseudo file system file names
# followed by their ASCII contents (i.e. their values) as appropriate.
# It targets sysfs (/sys ) in Linux but may be useful in other contexts.

version_str="1.12 20230603"

all=0
dir=0
empty=0
nosym=0
num_val=256   # may not be the best default
otherfs=0
show=0
is_root=0     # 0 for true; 1 for false (sorry C)
first_st_dev=0
verbose=0
writ_ind=0

# The following are for the getopt(1) command from util-linux which
# follows the function definitions, about 100 lines down.
script_name=$(basename "$0")
short="adehn:NosvVw"
long="all,dir,empty,help,num:,nosym,otherfs,show,verbose,version,write"


usage()
{
  echo "Usage: ls_name_value [-a] [-d] [-e] [-h] [-N] [-n NUM] [-o] [-s] "
  echo "                     [-v] [-V] [-w] [<name>*]"
  echo "  where:  -a, --all         include hidden filename (starting with '.')"
  echo "          -d, --dir         descend if <name> is directory"
  echo "                            descends 1 level; -dd descends 2 levels"
  echo "          -e, --empty       output <value> as '<empty>' rather than blank"
  echo "          -h, --help        print usage message"
  echo "          -N, --nosym       do not descend symlinks to directories"
  echo "          -n, --num=NUM     max bytes in value (def: 256)"
  echo "          -o, --otherfs     not descending to other fs is default, override"
  echo "          -s, --show        show directories, symlinks and specials"
  echo "          -v, --verbose     increase verbosity of output"
  echo "          -V, --version     print version string then exit"
  echo "          -w, --write       separator changed from : to + if writable"
  echo ""
  echo "Prints lines of the form '<name> : <value>' where <name> is a regular"
  echo "filename. The <value> is the contents of <name>, up to 256 bytes of "
  echo "printable ASCII. If one or more <name>s are given on the command line "
  echo "then they are used. If there are no names on the command line, then the "
  echo "regular files in the current directory are used. If one <name> is given "
  echo "and it is a directory then it is entered and the files in that directory "
  echo "are used. Designed for viewing sysfs in Linux (e.g. under /sys )."
}

get_root_perm()
{
  local s

  # 'stat -c ...' is a Linux extension. For FreeBSD use '-f'
  s=$(stat -c '%a' "$1")
  if [ -n "$s" ] ; then
    echo "${s:0:1}"
  else
    echo "0"
  fi
}

# Takes filename to display name:value of as $1. Optionally takes
# leading pad as $2
one_name_value()
{
  local grp
  local name
  local value
  local pad
  local separ

  if [ $# -gt 1 ] ; then
    pad=$2
  else
    pad=""
  fi
  if [ "${1:0:1}" = "/" ] || [ "${1:0:1}" = "." ] ; then
    name="${pad}$( basename "$1" ) "
  else
    name="${pad}${1} "
  fi
  separ=":"

  if [ -f "${1}" ] ; then
    if [ ${is_root} -ne 0 ] ; then    # false, so non-root
      if [ -r "${1}" ] ; then
        if [ "${writ_ind}" -gt 0 ] && [ -w "${1}" ] ; then
          separ="+"
        fi
        echo -n "${name}${separ} "
        value=$( head -c "${num_val}" "${1}" 2> /dev/null | tr -d '\0' )
        if [ $empty -eq 0 ] || [ -n "${value}" ] ; then
          value=$( echo  "${value}" | tr '\n' " " )
        else
          value="<empty>"
        fi
        if [[ ${value} = *[![:ascii:]]* ]]; then
          echo "<contains non-ASCII chars>"
        else
          echo "${value//[![:print:]]/}"
        fi
      elif [ -w "${1}" ] ; then
        echo "${pad}${1} : <write_only>"
      else
        echo "${pad}${1} : <cannot access>"
      fi
    else        # run by root
      grp=$( get_root_perm "${1}" )
      if [ "${grp}" -ge 4 ] ; then
        if [ "${writ_ind}" -gt 0 ] && [ "${grp}" -ge 6 ] ; then
          separ="+"
        fi
        echo -n "${name}${separ} "
        value=$( head -c "${num_val}" "${1}" 2> /dev/null | tr -d '\0' )
        if [ $empty -eq 0 ] || [ -n "${value}" ] ; then
          value=$( echo  "${value}" | tr '\n' " " )
        else
          value="<empty>"
        fi
        if [[ ${value} = *[![:ascii:]]* ]]; then
          echo "<contains non-ASCII chars>"
        else
          echo "${value//[![:print:]]/}"
        fi
      elif [ "${grp}" -ge 2 ] ; then
        echo "${pad}${1} : <write_only>"
      else
        echo "${pad}${1} : <cannot access>"
      fi
    fi
  elif [ $show -gt 0 ] ; then
    if [ -h "${1}" ] ; then
      echo -n "${pad}${1} : ---> "
      readlink "${1}"
    elif [ -d "${1}" ] ; then
      if [ $verbose -gt 0 ] ; then
        echo "${pad}${1} : --> [directory]"
      else
        echo "${pad}${1} : -->"
      fi
    elif [ -c "${1}" ] ; then
      echo "${pad}${1} : <char device>"
    elif [ -b "${1}" ] ; then
      echo "${pad}${1} : <block device>"
    elif [ -p "${1}" ] ; then
      echo "${pad}${1} : <named pipe>"
    elif [ -S "${1}" ] ; then
      echo "${pad}${1} : <socket>"
    fi
  fi
}


if (( EUID != 0 )); then
  is_root=1     # false
fi

# Reference: /usr/share/doc/util-linux/examples/getopt-example.bash
if ! TEMP=$(getopt -o $short --long $long --name "$script_name" -- "$@") ; then

# if [ $? -ne 0 ]; then
  echo 'Terminating...' >&2
  exit 1
fi

eval set -- "${TEMP}"

while :; do
  case "${1}" in
    -a | --all        ) (( all=all+1 )) ;          shift 1 ;;
    -d | --dir        ) (( dir=dir+1 )) ;          shift 1 ;;
    -e | --empty      ) (( empty=empty+1 )) ;      shift 1 ;;
    -h | --help       ) usage;                     exit 0 ;;
    -n | --num        )
                num_val="$2"
                if ! [ "$num_val" -eq "$num_val" ] 2> /dev/null ; then
                  echo "--num expects an integer as its argument"
                  exit 1
                elif [ "$num_val" -lt 1 ] ; then
                  echo "--num expects an integer > 0"
                  exit 1
                fi
                shift 2 ;;
    -N | --nosym      ) (( nosym=nosym+1 )) ;       shift 1 ;;
    -o | --otherfs    ) (( otherfs=otherfs+1 )) ;   shift 1 ;;
    -s | --show       ) (( show=show+1 )) ;         shift 1 ;;
    -v | --verbose    ) (( verbose=verbose+1 )) ;   shift 1 ;;
    -V | --version    ) echo "${version_str}" ;     exit 0 ;;
    -w | --write      ) (( writ_ind=writ_ind+1 )) ; shift 1 ;;
    --                ) shift;                      break ;;
    *                 ) echo "Error parsing $1";    exit 1 ;;
  esac
done

# If one arguent given and it's a directory, cd to it and swallow
if [ $# -eq 1 ] && [ -d "$1" ] && [ -x "$1" ] ; then
  # 'stat -c ...' is a Linux extension. For FreeBSD use '-f'
  first_st_dev=$( stat -c %d "$1" )
  if ! cd "$1" ; then
    echo "cd to $1 failed from $( pwd -P) "
    exit 1
  fi
  shift
fi

if [ $verbose -gt 0 ] ; then
  echo -n "> current working directory: "
  pwd
  if [ $verbose -gt 1 ] ; then
    echo -n "> physical working directory: "
    pwd -P
  fi
  echo ""
fi

SAVEIFS=$IFS
IFS=$'\n'


n=$#
if [ $# -gt 0 ] ; then
  # place command line arguments into a bash array
  for (( k=0 ; k<n ; k=k+1 )) ; do
    fileArray[k]="$1"
    shift
  done
else
  # place filenames in current directory into a bash array
  if [ $all -gt 0 ] ; then
    shopt -s dotglob
  fi
  # "shellcheck" says to use mapfile instead of following structure
  fileArray=($( echo -n "*" ))
  if [ $all -gt 0 ] ; then
    shopt -u dotglob
  fi
fi

tLen=${#fileArray[@]}
for (( k=0; k<tLen; k++ )) ; do
  name1="${fileArray[$k]}"
  if [ $verbose -gt 3 ] ; then
    echo ">> fileArray[$k]: ${name1}"
  fi
  if [ "${first_st_dev}" -eq 0 ] ; then
      first_st_dev=$( stat -c %d "${name1}" )
  fi
  if [ -h "${name1}" ] ; then
    symlnk1=0
    [ ${nosym} -gt 0 ]
    nosym_active1=$?
  else
    symlnk1=1
    nosym_active1=1
  fi
  if [ ${dir} -gt 0 ] && [ -d "${name1}" ] && [ -x "${name1}" ] && [ ${nosym_active1} -ne 0 ] ; then
    if [ ${otherfs} -eq 0 ] ; then
      a_st_dev=$( stat -c %d "${name1}" )
    else
      a_st_dev=${first_st_dev}
    fi
    if [ "${first_st_dev}" -eq "${a_st_dev}" ] ; then
      echo ">> descend to: ${name1}/"
      cur=$( pwd )
      if ! cd "${name1}" ; then
        echo "cd to ${name1} failed from $( pwd -P) "
        IFS=$SAVEIFS
        exit 1
      fi
      if [ ${verbose} -gt 2 ] && [ ${symlnk1} -eq 0 ] ; then
        echo -n "  > physical working directory: "
        pwd -P
      fi
      # place filenames in current directory into a bash array
      if [ $all -gt 0 ] ; then
        shopt -s dotglob
      fi
      fileArray2=($( echo -n "*" ))
      if [ $all -gt 0 ] ; then
        shopt -u dotglob
      fi
      t2Len=${#fileArray2[@]}
      for (( j=0; j<t2Len; j++ )) ; do
        name2="${fileArray2[$j]}"
        if [ -h "${name2}" ] ; then
          symlnk2=0
          [ ${nosym} -gt 0 ]
          nosym_active2=$?
        else
          symlnk2=1
          nosym_active2=1
        fi
        [ ${nosym} -gt 0 ] && [ -h "${name2}" ]
        nosym_active2=$?
        if [ ${dir} -gt 1 ] && [ -d "${name2}" ] && [ -x "${name2}" ] && [ ${nosym_active2} -ne 0 ] ; then
          if [ ${otherfs} -eq 0 ] ; then
            b_st_dev=$( stat -c %d "${name2}" )
          else
            b_st_dev=${first_st_dev}
          fi
          if [ "${first_st_dev}" -eq "${b_st_dev}" ] ; then
            echo "    >> descend to: ${name2}/"
            cur2=$( pwd )
            if ! cd "${name2}" ; then
              echo "cd to ${name2} failed from $( pwd -P) "
              IFS=$SAVEIFS
              exit 1
            fi
            if [ ${verbose} -gt 2 ] && [ ${symlnk2} -eq 0 ] ; then
              echo -n "      > physical working directory: "
              pwd -P
            fi
            if [ $all -gt 0 ] ; then
              shopt -s dotglob
            fi
            fileArray3=($( echo -n "*" ))
            if [ $all -gt 0 ] ; then
              shopt -u dotglob
            fi
            t3Len=${#fileArray3[@]}
            for (( m=0; m<t3Len; m++ )) ; do
              one_name_value "${fileArray3[m]}" "        "
            done
            if ! cd "${cur2}" ; then
              echo "cd to ${cur2} failed from $( pwd -P) "
              IFS=$SAVEIFS
              exit 1
            fi
            echo ""
          else
            echo "    >> not descending to: ${name2}/ : <different filesystem>"
          fi
        else
          one_name_value "${name2}" "    "
        fi
      done
      if ! cd "${cur}" ; then
        echo "cd to ${cur} failed from $( pwd -P) "
        IFS=$SAVEIFS
        exit 1
      fi
      echo ""
    else
      echo ">> not descending to: ${name1}/ : <different filesystem>"
    fi
  else
    one_name_value "${name1}"
  fi
done
IFS=$SAVEIFS
